package com.jcompressor.config;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.commons.lang.text.StrBuilder;
import org.exolab.castor.mapping.Mapping;
import org.exolab.castor.mapping.MappingException;
import org.exolab.castor.xml.MarshalException;
import org.exolab.castor.xml.Marshaller;
import org.exolab.castor.xml.Unmarshaller;
import org.exolab.castor.xml.ValidationException;
import org.exolab.castor.xml.XMLContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.jcompressor.JcompressorContext;
import com.jcompressor.digest.ChecksumUtils;
import com.jcompressor.exceptions.JcompressorException;
import com.jcompressor.model.config.Configuration;
import com.jcompressor.model.config.Resource;
import com.jcompressor.model.config.YUI;
import com.jcompressor.model.resources.ResourceConfiguration;
import com.jcompressor.model.resources.Template;
import com.jcompressor.type.ResourceType;
import com.jcompressor.utils.JavaScriptErrorReporter;
import com.jcompressor.utils.PropertyExpander;
import com.yahoo.platform.yui.compressor.CssCompressor;
import com.yahoo.platform.yui.compressor.JavaScriptCompressor;

public abstract class Configurator {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(Configurator.class);
	
	private Configuration configuration;
	private List<Resource> resources;
	private ResourceConfiguration resourceConfiguration;
	private PropertyExpander propertyExpander;
	private Mapping mapping;
	private XMLContext context;
	
	public Configurator() {
		try {
			final ClassLoader loader = Thread.currentThread().getContextClassLoader();
			this.resources = new ArrayList<Resource>();
			this.resourceConfiguration = new ResourceConfiguration();
			
			this.mapping = new Mapping();
			this.mapping.loadMapping(loader.getResource("META-INF/castor/mapping.xml"));
			
			this.context = new XMLContext();
			this.context.addMapping(this.mapping);
		}
		catch (final FileNotFoundException e) {
			e.printStackTrace();
		}
		catch (final IOException e) {
			e.printStackTrace();
		}
		catch (final MappingException e) {
			e.printStackTrace();
		}
	}
	
	public void configure() {
		this.loadConfiguration();
		this.scanAnnotations();
		this.buildResourceConfiguration();
		this.loadResources();
		this.compress();
		this.unloadResources();
	}
	
	protected abstract void scanAnnotations();
	
	protected abstract void loadResources();
	
	private void loadConfiguration() {
		final ClassLoader loader = Thread.currentThread().getContextClassLoader();
		FileReader reader = null;
		
		try {
			reader = new FileReader(loader.getResource("META-INF/jcompressor-config.xml").getFile());
			
			final Unmarshaller unmarshaller = this.context.createUnmarshaller();
			unmarshaller.setClass(Configuration.class);
			unmarshaller.setIgnoreExtraElements(true);
			
			this.configuration = (Configuration)unmarshaller.unmarshal(reader);
			this.propertyExpander = new PropertyExpander(this.configuration);
		}
		catch (final FileNotFoundException e) {
			e.printStackTrace();
		}
		catch (final MarshalException e) {
			e.printStackTrace();
		}
		catch (final ValidationException e) {
			e.printStackTrace();
		}
		finally {
			if (reader != null) {
				try {
					reader.close();
				}
				catch (final IOException e) {
					new JcompressorException(e);
				}
			}
		}
	}
	
	private void buildResourceConfiguration() {
		try {
			final String resourceFilePath = JcompressorContext.getResourceFilePath();
			final File resourceFile = new File(resourceFilePath);
			if ((resourceFile == null) || !resourceFile.exists()) {
				LOGGER.debug("Creating resource file " + resourceFilePath);
				FileUtils.forceMkdir(new File(StringUtils.substringBeforeLast(resourceFilePath, "/")));
				resourceFile.createNewFile();
			}
			
			final String resourceFileDirectory = JcompressorContext.getResourceFileDirectory();
			final File resourceDirectory = new File(resourceFileDirectory);
			if ((resourceDirectory == null) || !resourceDirectory.exists()) {
				LOGGER.debug("Creating resource directory " + resourceFileDirectory);
				FileUtils.forceMkdir(resourceDirectory);
			}
		}
		catch (final IOException e) {
			e.printStackTrace();
		}
	}
	
	private boolean compress() {
		final YUI yuiOptions = this.configuration.getBuild().getYui();
		
		for (final Resource resource : this.resources) {
			final long checksum = ChecksumUtils.createChecksum(resource.getContent());
			if (checksum > NumberUtils.INTEGER_ONE) {
				resource.setChecksum(checksum);
				this.resourceConfiguration.updateResource(resource);
				
				final String recourceFilename = (checksum + ".resource");
				final StrBuilder filePath = new StrBuilder(JcompressorContext.getResourceFileDirectory());
				filePath.append(recourceFilename);
				
				final File resourceFile = new File(filePath.toString());
				if (resourceFile.exists()) {
					Reader reader = null;
					
					try {
						reader = new InputStreamReader(new FileInputStream(resourceFile));
						
						// Reset the content with the compressed version
						resource.setContent(IOUtils.toString(reader));
					}
					catch (final Exception e) {
						throw new JcompressorException("Resource compression failed", e);
					}
					finally {
						IOUtils.closeQuietly(reader);
					}
				}
				else {
					Writer writer = null;
					Reader reader = null;
					
					try {
						writer = new FileWriter(filePath.toString());
						reader = new InputStreamReader(new ByteArrayInputStream(resource.getContent().getBytes()));
						
						LOGGER.debug("Compressing resource (" + filePath + ")");
						
						if (resource.getType().equals(ResourceType.SCRIPT)) {
							final JavaScriptCompressor compressor = new JavaScriptCompressor(reader, new JavaScriptErrorReporter(recourceFilename));
							compressor.compress(writer, yuiOptions.getLineBreak(), yuiOptions.isMunge(), yuiOptions.isVerbose(), yuiOptions.isPreserveAllSemiColons(), yuiOptions.isDisableOptimizations());
						}
						else if (resource.getType().equals(ResourceType.STYLE)) {
							final CssCompressor compressor = new CssCompressor(reader);
							compressor.compress(writer, yuiOptions.getLineBreak());
						}
						
						// Reset the content with the compressed version
						resource.setContent(IOUtils.toString(reader));
					}
					catch (final IOException e) {
						throw new JcompressorException("Resource compression failed", e);
					}
					finally {
						IOUtils.closeQuietly(reader);
						IOUtils.closeQuietly(writer);
					}
				}
			}
			else {
				this.resourceConfiguration.removeResource(resource);
			}
		}
		
		return true;
	}
	
	private void unloadResources() {
		FileWriter writer = null;
		
		try {
			writer = new FileWriter(JcompressorContext.getResourceFilePath());
			
			final Marshaller marshaller = this.context.createMarshaller();
			marshaller.setWriter(writer);
			marshaller.marshal(this.resourceConfiguration);
		}
		catch (final IOException e) {
			throw new JcompressorException(e);
		}
		catch (final MarshalException e) {
			throw new JcompressorException("Failed to marshall the jcompressor resource configuration", e);
		}
		catch (final ValidationException e) {
			throw new JcompressorException("Jcompressor resource configuration validation failure", e);
		}
		finally {
			if (writer != null) {
				try {
					writer.flush();
					writer.close();
				}
				catch (final IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	public void addTemplate(final Template template) {
		FileWriter writer = null;
		
		try {
			this.resourceConfiguration.addTemplate(template);
			
			writer = new FileWriter(JcompressorContext.getResourceFilePath());
			
			final Marshaller marshaller = this.context.createMarshaller();
			marshaller.setWriter(writer);
			marshaller.marshal(this.resourceConfiguration);
		}
		catch (final IOException e) {
			throw new JcompressorException(e);
		}
		catch (final MarshalException e) {
			throw new JcompressorException("Failed to marshall the jcompressor resource configuration", e);
		}
		catch (final ValidationException e) {
			throw new JcompressorException("Jcompressor resource configuration validation failure", e);
		}
		finally {
			if (writer != null) {
				try {
					writer.flush();
					writer.close();
				}
				catch (final IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	public Configuration getConfiguration() {
		return this.configuration;
	}
	
	public List<Resource> getResources() {
		return this.resources;
	}
	
	public ResourceConfiguration getResourcesConfiguration() {
		return this.resourceConfiguration;
	}
	
	public PropertyExpander getPropertyExpander() {
		return this.propertyExpander;
	}
	
	public void setResources(final List<Resource> resources) {
		this.resources = resources;
	}
	
}